package per.jdz.retryer.core;

import java.util.concurrent.Executor;
import java.util.function.Predicate;

import per.jdz.retryer.listener.RetryerListener;
import per.jdz.retryer.predicate.IPredicate;
import per.jdz.retryer.strategy.AbstractRetryerStrategy;

public class Retryer {

    private AbstractRetryerStrategy retryerStrategy;
    private Predicate predicateResult;
    private Predicate predicateThrowsException;
    private Executor executor;

    public Retryer(AbstractRetryerStrategy retryerStrategy, Predicate predicateResult,
            Predicate predicateThrowsException, RetryerListener retryerListener, Executor executor) {
        this.retryerStrategy = retryerStrategy;
        this.retryerStrategy.setRetryerListener(retryerListener);
        this.predicateResult = predicateResult;
        this.predicateThrowsException = predicateThrowsException;
        this.executor = executor;
    }

    private boolean execRetryStrategy() {
        return this.retryerStrategy.execute();
    }

    private boolean shouldRetry(Object result) {
        return null != predicateResult && ((IPredicate) predicateResult).apply(result);
    }

    private boolean shouldRetry(Throwable e) {
        return null != predicateThrowsException && ((IPredicate) predicateThrowsException).apply(e);
    }

    // reset
    private void reset() {
        retryerStrategy.reset();
    }

    public <T> T exec(RetryerSupplier supplier) {
        T result = null;
        try {
            result = (T) supplier.exec();
            if (shouldRetry(result)) {
                if (null != executor) {
                    executor.execute(() -> {
                        if (execRetryStrategy()) {
                            asyncExec(supplier);
                        }
                    });
                } else {
                    if (execRetryStrategy()) {
                        exec(supplier);
                    }
                }
            }
        } catch (Throwable e) {
            if (e.getClass().equals(RuntimeException.class)) {
                e = e.getCause();
            }
            if (shouldRetry(e)) {
                if (null != executor) {
                    executor.execute(() -> {
                        if (execRetryStrategy()) {
                            asyncExec(supplier);
                        }
                    });
                } else {
                    if (execRetryStrategy()) {
                        exec(supplier);
                    }
                }
            }
        }
        reset();
        return result;
    }

    private <T> void asyncExec(RetryerSupplier supplier) {
        try {
            T result = (T) supplier.exec();
            if (shouldRetry(result)) {
                if (execRetryStrategy()) {
                    asyncExec(supplier);
                }
            }
        } catch (Throwable e) {
            if (e instanceof RuntimeException) {
                e = e.getCause();
            }
            if (shouldRetry(e)) {
                if (execRetryStrategy()) {
                    asyncExec(supplier);
                }
            }
        }
    }
}
